TypeScript · 11984 bytes Raw Blame History
1 'use client';
2
3 import { useState, useEffect } from 'react';
4 import Link from 'next/link';
5 import Image from 'next/image';
6 import { useParams } from 'next/navigation';
7 import { getPersonDetail, PersonDetailWithContributions } from '@/lib/api';
8 import Header from '@/components/Header';
9 import ContributionForm from '@/components/ContributionForm';
10 import ContributionDisplay from '@/components/ContributionDisplay';
11
12 export default function PersonPage() {
13 const params = useParams();
14 const personId = parseInt(params.id as string);
15
16 const [person, setPerson] = useState<PersonDetailWithContributions | null>(null);
17 const [loading, setLoading] = useState(true);
18 const [error, setError] = useState<string | null>(null);
19 const [pdfError, setPdfError] = useState(false);
20 const [pdfUrl, setPdfUrl] = useState<string | null>(null);
21 const [loadingPdf, setLoadingPdf] = useState(false);
22
23 useEffect(() => {
24 async function fetchData() {
25 try {
26 const personData = await getPersonDetail(personId);
27 setPerson(personData);
28 } catch (err) {
29 setError(err instanceof Error ? err.message : 'Failed to load person details');
30 console.error(err);
31 } finally {
32 setLoading(false);
33 }
34 }
35
36 fetchData();
37 }, [personId]);
38
39 useEffect(() => {
40 async function fetchPdfUrl() {
41 if (person && person.pdf_key && !pdfUrl) {
42 setLoadingPdf(true);
43 try {
44 const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api';
45 setPdfUrl(`${apiUrl}/memorial/persons/${person.id}/pdf/`);
46 } catch (err) {
47 console.error('Failed to set PDF URL:', err);
48 setPdfError(true);
49 } finally {
50 setLoadingPdf(false);
51 }
52 }
53 }
54
55 fetchPdfUrl();
56 }, [person, pdfUrl]);
57
58 const handleContributionSuccess = async () => {
59 // Reload person data to get updated contributions
60 try {
61 const personData = await getPersonDetail(personId);
62 setPerson(personData);
63 } catch (err) {
64 console.error('Failed to reload person data:', err);
65 }
66 };
67
68 if (loading) {
69 return (
70 <div className="min-h-screen bg-vmi-cream flex items-center justify-center">
71 <p className="text-gray-600 text-xl">Loading...</p>
72 </div>
73 );
74 }
75
76 if (error || !person) {
77 return (
78 <div className="min-h-screen bg-vmi-cream flex items-center justify-center">
79 <div className="text-center">
80 <p className="text-red-600 mb-4 text-xl">{error || 'Person not found'}</p>
81 <Link href="/" className="text-vmi-red hover:text-vmi-dark-red underline font-semibold">
82 Return to Home
83 </Link>
84 </div>
85 </div>
86 );
87 }
88
89 const displayName = (() => {
90 const name = person.full_display_name || person.display_name;
91 if (person.rank && person.full_display_name) {
92 return name.replace(person.rank + ' ', '').replace(person.rank + ', ', '');
93 }
94 return person.full_display_name ? name : person.display_name;
95 })();
96
97 return (
98 <div className="min-h-screen bg-vmi-cream">
99 <Header
100 breadcrumbs={[
101 { label: 'Home', href: '/' },
102 { label: person.conflict_name, href: `/memorial/conflict/${person.conflict}` },
103 { label: person.display_name }
104 ]}
105 />
106
107 {/* Main Content */}
108 <main className="max-w-6xl mx-auto px-4 py-12">
109 {/* Person Header */}
110 <div className="bg-vmi-light-gold border-2 border-vmi-gold rounded-lg p-8 mb-12 shadow-xl">
111 <h1 className="text-4xl font-black text-vmi-red mb-2 flex items-center gap-2">
112 {displayName}
113 {person.pdf_key && (
114 <span title="Memorial document available" className="inline-block align-middle">
115 {/* Simple document icon SVG */}
116 <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#b91c1c" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="feather feather-file-text">
117 <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
118 <polyline points="14 2 14 8 20 8" />
119 <line x1="16" y1="13" x2="8" y2="13" />
120 <line x1="16" y1="17" x2="8" y2="17" />
121 <line x1="10" y1="9" x2="9" y2="9" />
122 </svg>
123 </span>
124 )}
125 </h1>
126
127 {/* Rank and Unit subtitle */}
128 {(person.rank || person.unit) && (
129 <div className="mb-6">
130 {person.rank && (
131 <p className="text-xl font-bold text-gray-700">{person.rank}</p>
132 )}
133 {person.unit && (
134 <p className="text-lg text-gray-600 italic">{person.unit}</p>
135 )}
136 </div>
137 )}
138
139 <div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-gray-800">
140 <div className="space-y-3">
141 {person.class_year && (
142 <p className="text-lg">
143 <span className="font-bold text-gray-700">Class Year:</span> {person.class_year}{person.class_letter || ''}
144 </p>
145 )}
146 <p className="text-lg">
147 <span className="font-bold text-gray-700">Conflict:</span> {person.conflict_name}
148 </p>
149 </div>
150 <div className="space-y-3">
151 {person.death_date_display && (
152 <p className="text-lg">
153 <span className="font-bold text-gray-700">Date of Death:</span>{' '}
154 {person.death_date_display}
155 </p>
156 )}
157 </div>
158 </div>
159 </div>
160
161 {/* Death Description Section */}
162 {person.death_description && (
163 <div className="bg-white border-2 border-gray-300 rounded-lg p-8 mb-12 shadow-xl">
164 <h2 className="text-2xl font-bold mb-4 text-vmi-red">
165 Circumstances of Death
166 </h2>
167 <p className="text-lg text-gray-800 leading-relaxed italic">
168 {person.death_description}
169 </p>
170 </div>
171 )}
172
173 {/* Awards Section */}
174 {person.awards && person.awards.length > 0 && (
175 <div className="bg-white border-2 border-gray-300 rounded-lg p-8 mb-12 shadow-xl">
176 <h2 className="text-2xl font-bold mb-6 text-vmi-red">
177 Awards for Heroism &amp; Gallantry
178 </h2>
179 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
180 {person.awards.map((award) => {
181 const getImagePath = (filename: string) => {
182 const extensions = ['.jpg', '.png', '.jpeg', '.gif'];
183 for (const ext of extensions) {
184 if (filename.toLowerCase().endsWith(ext)) {
185 return `/${filename}`;
186 }
187 }
188 return `/${filename}.jpg`;
189 };
190
191 return (
192 <Link
193 key={award.award_id}
194 href={`/awards/${award.award_id}`}
195 className="flex items-center gap-4 p-4 rounded-lg border-2 border-gray-200 hover:border-vmi-gold hover:bg-vmi-light-gold transition-all duration-200 group"
196 >
197 <div className="relative w-16 h-20 flex-shrink-0">
198 <Image
199 src={getImagePath(award.award_image_filename)}
200 alt={award.award_name}
201 fill
202 className="object-contain"
203 sizes="64px"
204 />
205 </div>
206 <div>
207 <h3 className="font-bold text-gray-800 group-hover:text-vmi-red transition-colors">
208 {award.award_name}
209 {award.count > 1 && (
210 <span className="ml-2 text-sm text-vmi-gold">
211 (×{award.count})
212 </span>
213 )}
214 </h3>
215 {award.date_awarded && (
216 <p className="text-sm text-gray-500">
217 {new Date(award.date_awarded).toLocaleDateString()}
218 </p>
219 )}
220 {award.citation && (
221 <p className="text-sm text-gray-600 italic line-clamp-2 mt-1">
222 &ldquo;{award.citation}&rdquo;
223 </p>
224 )}
225 </div>
226 </Link>
227 );
228 })}
229 </div>
230 </div>
231 )}
232
233 {/* PDF Viewer */}
234 <div className="bg-white border-2 border-gray-300 rounded-lg p-8 shadow-xl mb-12">
235 <h2 className="text-3xl font-bold mb-6 text-center text-vmi-red">
236 Memorial Portrait
237 </h2>
238
239 {person.pdf_key ? (
240 <div className="relative">
241 {loadingPdf ? (
242 <div className="border-3 border-gray-400 border-dashed rounded-lg p-16 text-center bg-gray-50">
243 <p className="text-gray-700 text-lg">Loading PDF...</p>
244 </div>
245 ) : pdfError ? (
246 <div className="border-3 border-gray-400 border-dashed rounded-lg p-16 text-center bg-gray-50">
247 <p className="text-gray-700 mb-4 text-lg">
248 Unable to load PDF viewer.
249 </p>
250 {pdfUrl && (
251 <a
252 href={pdfUrl}
253 target="_blank"
254 rel="noopener noreferrer"
255 className="inline-block bg-vmi-red text-white px-6 py-3 rounded hover:bg-vmi-dark-red hover:text-white transition-colors font-semibold"
256 >
257 Open PDF in New Tab
258 </a>
259 )}
260 </div>
261 ) : pdfUrl ? (
262 <div className="border-2 border-gray-400 rounded-lg overflow-hidden">
263 <iframe
264 src={`${pdfUrl}#toolbar=0&navpanes=0`}
265 className="w-full h-[800px]"
266 onError={() => setPdfError(true)}
267 title={`Memorial document for ${person.display_name}`}
268 />
269 <div className="p-6 bg-gray-100 text-center border-t-2 border-gray-400">
270 <a
271 href={pdfUrl}
272 target="_blank"
273 rel="noopener noreferrer"
274 className="inline-block bg-vmi-red text-white px-6 py-3 rounded hover:bg-vmi-dark-red hover:text-white transition-colors font-semibold"
275 >
276 Open PDF in Full Screen
277 </a>
278 </div>
279 </div>
280 ) : null}
281 </div>
282 ) : (
283 <div className="border-3 border-gray-400 border-dashed rounded-lg p-16 text-center bg-gray-50">
284 <p className="text-gray-700 text-lg">
285 No memorial document available yet.
286 </p>
287 </div>
288 )}
289 </div>
290
291 {/* Community Contributions Section */}
292 <ContributionForm
293 personId={person.id}
294 personName={displayName}
295 onSuccess={handleContributionSuccess}
296 />
297
298 {/* Display Approved Contributions */}
299 {person.contributions && person.contributions.length > 0 && (
300 <div className="bg-white border-2 border-gray-300 rounded-lg p-8 shadow-xl mt-8">
301 <ContributionDisplay contributions={person.contributions} />
302 </div>
303 )}
304 </main>
305 </div>
306 );
307 }